﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Fasterflect;
using Preactor.ObjectModel.Attributes;
using Preactor.ObjectModel.Interfaces;

namespace Preactor.ObjectModel.Entities
{
    public abstract class PreactorRecord : IPreactorRecord
    {
        protected PreactorRecord(PrimaryKey id, PreactorObjectModel preactorObjectModel)
        {
            ThisType = GetType();

            Id = id;
            PreactorObjectModel = preactorObjectModel;
            FormatName = PreactorObjectModel.GetFormatNameByType(ThisType);
            FormatNumber = PreactorObjectModel.GetFormatNumberByType(ThisType);

            InitialiseCache();
        }

        private event EventHandler Deleted;

        public string FormatName { get; private set; }

        public int FormatNumber { get; private set; }

        [PreactorField("PRIMARY KEY", FieldDeclarationType.FieldClassification, false)]
        public PrimaryKey Id { get; private set; }

        public PreactorObjectModel PreactorObjectModel { get; private set; }

        public int RecordNumber
        {
            get
            {
                return PreactorObjectModel.Preactor.GetRecordNumber(FormatName, Id);
            }
        }

        private Dictionary<string, object> Cache { get; set; }

        private Type ThisType { get; set; }

        public virtual void Delete()
        {
            // Delete the record in Preactor
            PreactorObjectModel.DeleteRecord(this);

            // Trigger Event
            OnDeleted(RecordEventArgs.Empty);
        }

        public void SetValue(string propertyName, object value)
        {
            PropertyInfo propertyInfo = ThisType.Property(propertyName);

            // Check whether we have a Preactor field or not
            if (!PreactorObjectModel.TypePreactorFields[ThisType].Contains(propertyName))
            {
                throw new ArgumentException(string.Format("The propertyName: '{0}' is not defined in the Preactor Object Model.", propertyName));
            }

            if (propertyInfo == null)
            {
                throw new ArgumentException(string.Format("The propertyName: '{0}' specified does not exist in: {1}", propertyName, ThisType.FullName));
            }

            FormatFieldPair formatFieldPair = PreactorObjectModel.FormatFieldPairDefinitions[ThisType][propertyName];

            SetPropertyValue(formatFieldPair, value);

            // Update the Cache value
            if (PreactorObjectModel.IsPropertyCacheEnabled && Cache.ContainsKey(propertyName))
            {
                if (propertyInfo.Type() != value.GetType())
                {
                    throw new ArgumentException(string.Format("The object type ({0}) is not the same as the property ({1}) type ({2}).", value.GetType(), propertyName, propertyInfo.Type()));
                }

                Cache[propertyName] = value;
            }
        }

        public void SetFieldValue<T>(string fieldName, T value)
        {
            int fieldNumber = GetPreactorFieldNumber(fieldName);
            var formatFieldPair = new FormatFieldPair(FormatNumber, fieldNumber);

            SetPropertyValue(formatFieldPair, value);
        }

        public object GetValue(string propertyName)
        {
            PropertyInfo propertyInfo = ThisType.Property(propertyName);

            if (PreactorObjectModel.IsPropertyCacheEnabled && Cache.ContainsKey(propertyName) && Cache[propertyName] != null)
            {
                if (PreactorObjectModel.IsStatisticsEnabled)
                {
                    var stringName = string.Format("{0} - GET CACHED - {1}", ThisType.Name, propertyName);

                    if (PreactorObjectModel.Statistics.ContainsKey(stringName))
                    {
                        PreactorObjectModel.Statistics[stringName]++;
                    }
                    else
                    {
                        PreactorObjectModel.Statistics[stringName] = 1;
                    }
                }

                return Cache[propertyName];
            }

            var formatFieldPair = PreactorObjectModel.FormatFieldPairDefinitions[ThisType][propertyName];

            var value = GetPreactorValue(propertyInfo.PropertyType.FullName, formatFieldPair);

            if (Cache.ContainsKey(propertyName))
            {
                if (PreactorObjectModel.IsPropertyCacheEnabled == false && Cache[propertyName] != null)
                {
                    Cache[propertyName] = null;
                }

                if (PreactorObjectModel.IsPropertyCacheEnabled  && Cache[propertyName] == null)
                {
                    // Need to store the value first time around
                    Cache[propertyName] = value;
                }
            }

            if (PreactorObjectModel.IsStatisticsEnabled)
            {
                var stringName = string.Format("{0} - GET - {1}", ThisType.Name, propertyName);

                if (PreactorObjectModel.Statistics.ContainsKey(stringName))
                {
                    PreactorObjectModel.Statistics[stringName]++;
                }
                else
                {
                    PreactorObjectModel.Statistics[stringName] = 1;
                }
            }

            return value;
        }

        public T GetFieldValue<T>(string fieldName)
        {
            var formatFieldPair = GetFormatFieldPair(fieldName);

            return (T) GetPreactorValue(typeof (T).FullName, formatFieldPair);
        }

        public T GetFieldValue<T>(string fieldName, int index)
        {
            var formatFieldPair = GetFormatFieldPair( fieldName );

            return (T)GetPreactorValue(typeof(T).FullName, formatFieldPair, index);
        }

        private void InitialiseCache()
        {
            Cache = new Dictionary<string, object>();

            var cachedProperties = PreactorObjectModel.GetCachedProperties(ThisType).ToList();

            if (!cachedProperties.Any())
            {
                return;
            }

            foreach (var propertyName in cachedProperties)
            {
                // Never cace ID or RecordNumber
                if (propertyName == "Id" || propertyName == "RecordNumber")
                {
                    continue;
                }

                Cache[propertyName] = null;
            }
        }

        private FormatFieldPair GetFormatFieldPair(string fieldName)
        {
            int fieldNumber = GetPreactorFieldNumber(fieldName);

            return new FormatFieldPair(FormatNumber, fieldNumber);
        }

        private void OnDeleted(EventArgs e)
        {
            EventHandler handler = Deleted;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        protected List<List<T>> ReadMatrixFieldString<T>(FormatFieldPair formatFieldPair)
        {
            MatrixDimensions mtx = PreactorObjectModel.Preactor.MatrixFieldSize(formatFieldPair, RecordNumber);

            var lstRows = new List<List<T>>();
            var lstColumn = new List<T>();

            for (int iy = 1; iy <= mtx.Y; iy++)
            {
                // row
                lstColumn.Clear();

                for (int ix = 1; ix <= mtx.X; ix++)
                {
                    // column
                    lstColumn.Add(GetMatrixFieldValue<T>(formatFieldPair, ix, iy));
                }

                lstRows.Add(lstColumn);
            }

            return lstRows;
        }

        private T GetMatrixFieldValue<T>(FormatFieldPair formatFieldPair, int x, int y)
        {
            Type type = typeof(T);

            if (type == typeof(string))
            {
                return (T)(object)PreactorObjectModel.Preactor.ReadFieldString(formatFieldPair, Id, x, y);
            }

            if (type == typeof(int))
            {
                return (T)(object)PreactorObjectModel.Preactor.ReadFieldInt(formatFieldPair, Id, x, y);
            }

            if (type == typeof(bool))
            {
                return (T)(object)PreactorObjectModel.Preactor.ReadFieldBool(formatFieldPair, Id, x, y);
            }

            if (type == typeof(double))
            {
                return (T)(object)PreactorObjectModel.Preactor.ReadFieldDouble(formatFieldPair, Id, x, y);
            }

            throw new ArgumentException(string.Format("No record was found for format field pair: {0} and record number: {1}", formatFieldPair, RecordNumber));
        }

        private object GetPreactorValue(string propertyTypeFullName, FormatFieldPair formatFieldPair)
        {
            IPreactor preactor = PreactorObjectModel.Preactor;
            object value = null;

            if (formatFieldPair.FieldNumber == -1)
            {
                return GetTypeDefaultValue(propertyTypeFullName);
            }

            switch (propertyTypeFullName)
            {
                case "System.String":
                    value = preactor.ReadFieldString(formatFieldPair, Id);
                    break;
                case "System.Int32":
                    value = preactor.ReadFieldInt(formatFieldPair, Id);
                    break;
                case "System.DateTime":
                    value = preactor.ReadFieldDateTime(formatFieldPair, Id);
                    break;
                case "System.Boolean":
                    value = preactor.ReadFieldBool(formatFieldPair, Id);
                    break;
                case "System.Double":
                    value = preactor.ReadFieldDouble(formatFieldPair, Id);
                    break;
                case "System.TimeSpan":
                    value = TimeSpan.FromDays(preactor.ReadFieldDouble(formatFieldPair, Id));
                    break;
                case "Preactor.PrimaryKey":
                    value = new PrimaryKey(preactor.ReadFieldDouble(formatFieldPair, Id));
                    break;
            }

            return value;
        }

        private object GetPreactorValue(string propertyTypeFullName, FormatFieldPair formatFieldPair, int index)
        {
            IPreactor preactor = PreactorObjectModel.Preactor;
            object value = null;

            if (formatFieldPair.FieldNumber == -1)
            {
                return GetTypeDefaultValue(propertyTypeFullName);
            }

            switch (propertyTypeFullName)
            {
                case "System.String":
                    value = preactor.ReadFieldString(formatFieldPair, Id, index);
                    break;
                case "System.Int32":
                    value = preactor.ReadFieldInt(formatFieldPair, Id, index);
                    break;
                case "System.DateTime":
                    value = preactor.ReadFieldDateTime(formatFieldPair, Id, index);
                    break;
                case "System.Boolean":
                    value = preactor.ReadFieldBool(formatFieldPair, Id, index);
                    break;
                case "System.Double":
                    value = preactor.ReadFieldDouble(formatFieldPair, Id, index);
                    break;
                case "System.TimeSpan":
                    value = TimeSpan.FromDays(preactor.ReadFieldDouble(formatFieldPair, Id, index));
                    break;
                case "Preactor.PrimaryKey":
                    value = new PrimaryKey(preactor.ReadFieldDouble(formatFieldPair, Id, index));
                    break;
            }

            return value;
        }

        private object GetTypeDefaultValue(string typeFullName)
        {
            object value = null;

            switch (typeFullName)
            {
                case "System.String":
                    value = default(string);
                    break;
                case "System.Int32":
                    value = -1;
                    break;
                case "System.DateTime":
                    value = default(DateTime);
                    break;
                case "System.Boolean":
                    value = default(bool);
                    break;
                case "System.Double":
                    value = default(double);
                    break;
                case "System.TimeSpan":
                    value = default(TimeSpan);
                    break;
                case "Preactor.PrimaryKey":
                    throw new ArgumentException("The primary key is not defined.");
            }

            return value;
        }

        private int GetPreactorFieldNumber(string fieldName)
        {
            int fieldNumber = PreactorObjectModel.Preactor.GetFieldNumber(FormatName, fieldName);

            if (fieldNumber == 0)
            {
                throw new ArgumentException(string.Format("The field Name: {0}, is not defined in the format: {1}", fieldName, FormatName));
            }

            return fieldNumber;
        }

        private void SetPropertyValue(FormatFieldPair formatFieldPair, object value)
        {
            switch (value.GetType().FullName)
            {
                case "System.Int32":
                    PreactorObjectModel.Preactor.WriteField(formatFieldPair, Id, (int)value);
                    break;
                case "System.Boolean":
                    PreactorObjectModel.Preactor.WriteField(formatFieldPair, Id, (bool)value);
                    break;
                case "System.Double":
                    PreactorObjectModel.Preactor.WriteField(formatFieldPair, Id, (double)value);
                    break;
                case "System.String":
                    PreactorObjectModel.Preactor.WriteField(formatFieldPair, Id, (string)value);
                    break;
                case "System.DateTime":
                    PreactorObjectModel.Preactor.WriteField(formatFieldPair, Id, (DateTime)value);
                    break;
                case "System.TimeSpan":
                    var duration = (TimeSpan)value;
                    PreactorObjectModel.Preactor.WriteField(formatFieldPair, Id, duration.TotalDays);
                    break;
            }
        }
    }
}